package task_executor

import (
	"fmt"
	"github.com/tebeka/selenium"
	"github.com/tebeka/selenium/chrome"
	"math"
	"math/rand"
	"regexp"
	"strings"
	"time"
	"view-crawler-backend/internal/bo"
	"view-crawler-backend/internal/constant"
	"view-crawler-backend/internal/tools"
)

// TaskExecutor 任务执行器，封装爬取逻辑以及收纳数据
type TaskExecutor struct {
	nodes []any
	wd    selenium.WebDriver
	done  chan struct{}
	// 用于控制任务的暂停与继续
	controlStream chan bool
	// 用于给日志传输协程传递信息
	msgSendStream chan string
	// 用于给数据持久化协程传递信息
	dataStream chan map[string]any
	// 任务被暂停/取消时会被置为false
	execStatus bool

	// 此次是该任务的第几次执行
	execId int
}

// Exec 执行爬虫任务
func (te *TaskExecutor) Exec() {
	te.sendLog(constant.TASK_INIT, "任务初始化完成")
	te.sendLog(constant.TASK_BEGIN, "")
	for _, node := range te.nodes {
		te.exec(node)
		if !te.execStatus {
			return
		}
	}
	te.sendLog(constant.TASK_END, "")
}

func (te *TaskExecutor) exec(node any) {
	// 执行一些通用的预处理
	te.beforeHandleNode(node.(bo.NodeInf))

	switch n := node.(type) {
	case *bo.LoopNode:
		switch n.LoopType {
		case constant.LoopTypeLoopURL:
			te.handleLoopURL(n)
		case constant.LoopTypeLoopPaging:
			te.handleLoopPaging(n)
		case constant.LoopTypeLoopList:
			te.handleLoopList(n)
		}
	case *bo.OpenPageNode:
		te.handleOpenPage(n)
	case *bo.ClickElementNode:
		te.handleClickElement(n)
	case *bo.ExtractDataNode:
		te.handleExtractData(n)
	case *bo.ConditionNode:
		te.handleCondition(n)
	}

	if te.execStatus {
		// 每个节点执行后都是一个可抢占点，用户可以暂停/终止任务
		te.judgeTaskExecStatus()
	}
}

func (te *TaskExecutor) judgeTaskExecStatus() {
	select {
	case <-te.done:
		te.sendLog(constant.TASK_BREAK, "用户终止了任务")
		te.execStatus = false
	case flag := <-te.controlStream:
		// 用户暂停了任务
		if !flag {
			te.sendLog(constant.TASK_PAUSE, "用户暂停了任务")
			te.execStatus = false
			for {
				select {
				case <-te.done:
					te.sendLog(constant.TASK_BREAK, "用户终止了任务")
					te.execStatus = false
					return
				case flag = <-te.controlStream:
					if flag {
						te.sendLog(constant.TASK_CONTINUE, "用户继续了任务")
						te.execStatus = true
						return
					}
				}
			}
		}
	default:
	}
}

func (te *TaskExecutor) handleLoopURL(node *bo.LoopNode) {
	loopTimes := len(node.URLs)
	if node.LoopTimes > 0 {
		loopTimes = min(loopTimes, node.LoopTimes)
	}
	te.sendLog(constant.LOOP_URL, fmt.Sprintf("开始循环打开网页，共%d个", loopTimes))
	for i := 0; i < loopTimes; i++ {
		for i, subNode := range node.SubNodes {
			// 找到循环打开网页节点中的打开网页节点，为其配置URL
			if openPageNode, ok := subNode.(*bo.OpenPageNode); ok && openPageNode.URL == "" {
				te.sendLog(constant.LOOP_URL, fmt.Sprintf("循环打开网页,第%d个，共%d个", i+1, loopTimes))
				openPageNode.URL = node.URLs[i]
			}
			te.exec(subNode)
			if !te.execStatus {
				return
			}
		}
	}
	te.sendLog(constant.LOOP_URL, "循环打开网页结束")
}

func (te *TaskExecutor) handleLoopPaging(node *bo.LoopNode) {
	loopTimes := math.MaxInt
	if node.LoopTimes > 0 {
		loopTimes = node.LoopTimes
	}
	// 循环点击的元素
	xpath := node.XpathList[0]
	te.sendLog(constant.LOOP_PAGING, fmt.Sprintf("开始循环翻页,%s", xpath))
	for i := 0; i < loopTimes; i++ {
		for _, subNode := range node.SubNodes {
			if clickElementNode, ok := subNode.(*bo.ClickElementNode); ok && clickElementNode.IsPaging {
				ele, err := te.wd.FindElement(selenium.ByXPATH, xpath)
				// 找不到循环翻页按钮或达到最大翻页次数（最后一次的结尾不用再点击循环）
				if err != nil || i == loopTimes-1 {
					te.sendLog(constant.LOOP_PAGING, "循环翻页结束")
					return
				}
				// 此次点击是进行翻页
				clickElementNode.Ele = ele
				te.sendLog(constant.LOOP_PAGING, fmt.Sprintf("翻页:%s,第%d次", xpath, i+1))
			}
			te.exec(subNode)
			if !te.execStatus {
				return
			}
		}
	}
}

func (te *TaskExecutor) handleLoopList(node *bo.LoopNode) {
	// TODO 目前由于前端的元素预选算法的原因，xpath与循环的元素项不是一一对应关系,在循环元素列表时无法进行循环次数控制，之后等元素选择算法优化后再处理
	te.sendLog(constant.LOOP_ELEMENT, "开始循环元素列表")
	elementList := make([]selenium.WebElement, 0)
	for _, xpath := range node.XpathList {
		elements, err := te.wd.FindElements(selenium.ByXPATH, xpath)
		if err != nil {
			te.sendLog(constant.EXCEPTION, fmt.Sprintf("元素定位失败：%s", xpath))
			continue
		}
		elementList = append(elementList, elements...)
	}
	loopTimes := len(elementList)
	if node.LoopTimes > 0 {
		loopTimes = min(loopTimes, node.LoopTimes)
	}
	for i := 0; i < loopTimes; i++ {
		for _, subNode := range node.SubNodes {
			if clickElementNode, ok := subNode.(*bo.ClickElementNode); ok && clickElementNode.Xpath == "" {
				clickElementNode.Ele = elementList[i]
			}
			if extractDataNode, ok := subNode.(*bo.ExtractDataNode); ok && extractDataNode.Xpath == "" {
				extractDataNode.Ele = elementList[i]
			}
			if conditionNode, ok := subNode.(*bo.ConditionNode); ok {
				conditionNode.Ele = elementList[i]
			}
			te.exec(subNode)
			if !te.execStatus {
				return
			}
		}
	}
	te.sendLog(constant.LOOP_ELEMENT, "循环元素列表结束")
}

func (te *TaskExecutor) handleOpenPage(node *bo.OpenPageNode) {
	te.sendLog(constant.OPEN_PAGE, node.URL)
	if err := te.wd.Get(node.URL); err != nil {
		te.sendLog(constant.EXCEPTION, fmt.Sprintf("打开页面失败：%s", node.URL))
	}
	te.sendLog(constant.OPEN_PAGE, "打开网页成功")
}

func (te *TaskExecutor) handleExtractData(node *bo.ExtractDataNode) {
	element := node.Ele
	// 这是一个独立的数据提取节点
	if element == nil {
		ele, err := te.wd.FindElement(selenium.ByXPATH, node.Xpath)
		if err != nil {
			te.sendLog(constant.EXCEPTION, fmt.Sprintf("元素定位失败：%s", node.Xpath))
			return
		}
		element = ele
	}

	// 数据集的一行
	row := make(map[string]any)
	// 构建每一列的数据
	for _, data := range node.Data {
		switch data.Type {
		case constant.DataTypeText:
			text, err := element.Text()
			if err != nil {
				te.sendLog(constant.EXCEPTION, "获取元素文本失败")
				row[data.Name] = "null"
				break
			}
			row[data.Name] = text
		case constant.DataTypeURL:
			eles, err := element.FindElements(selenium.ByCSSSelector, "a,img")
			if err != nil {
				te.sendLog(constant.EXCEPTION, "元素定位失败")
				row[data.Name] = "null"
				break
			}
			tagName, _ := element.TagName()
			if tagName == "a" || tagName == "img" {
				eles = append([]selenium.WebElement{element}, eles...)
			}
			for _, ele := range eles {
				tagName, err := ele.TagName()
				if err != nil {
					te.sendLog(constant.EXCEPTION, "获取元素属性失败")
					row[data.Name] = "null"
					break
				}
				if tagName == "a" {
					link, err := ele.GetAttribute("href")
					if err != nil {
						te.sendLog(constant.EXCEPTION, "获取元素属性失败")
						row[data.Name] = "null"
						break
					}
					row[data.Name] = link
					break
				}
				if tagName == "img" {
					link, err := ele.GetAttribute("src")
					if err != nil {
						te.sendLog(constant.EXCEPTION, "获取元素属性失败")
						row[data.Name] = "null"
						break
					}
					row[data.Name] = link
					break
				}
			}
		case constant.ExtraDataTypePageURL:
			if url, err := te.wd.CurrentURL(); err == nil {
				row[data.Name] = url
				break
			}
			row[data.Name] = "null"
			te.sendLog(constant.EXCEPTION, "获取页面URL失败")
		case constant.ExtraDataTypePageTitle:
			if url, err := te.wd.Title(); err == nil {
				row[data.Name] = url
				break
			}
			row[data.Name] = "null"
			te.sendLog(constant.EXCEPTION, "获取页面Title失败")
		case constant.ExtraDataTypeExtractTime:
			row[data.Name] = time.Now().Format(time.DateTime)
		}

		// 执行后处理
		prefix, suffix, originValue := "", "", row[data.Name].(string)
		for _, item := range data.Reprocess {
			mp := item.(map[string]interface{})
			param := mp["param"].(string)
			switch int(mp["type"].(float64)) {
			case constant.ReprocessClearEmpty:
				originValue = strings.TrimSpace(originValue)
			case constant.ReprocessAddPrefix:
				prefix = param
			case constant.ReprocessAddSuffix:
				suffix = param
			case constant.ReprocessRegExp:
				reg, err := regexp.Compile(param)
				if err != nil {
					break
				}
				res := reg.FindAllString(originValue, -1)
				originValue = strings.Join(res, " ")
			}
		}
		row[data.Name] = prefix + originValue + suffix
	}
	te.sendLog(constant.EXTRACT_DATA, fmt.Sprintf("%v", row))
	te.dataStream <- row
}

func (te *TaskExecutor) handleClickElement(node *bo.ClickElementNode) {
	element := node.Ele
	if element == nil {
		// 是一个独立的ClickElement节点
		ele, err := te.wd.FindElement(selenium.ByXPATH, node.Xpath)
		if err != nil {
			te.sendLog(constant.EXCEPTION, fmt.Sprintf("元素定位失败：%s", node.Xpath))
			return
		}
		element = ele
	}

	if err := element.Click(); err != nil {
		te.sendLog(constant.EXCEPTION, fmt.Sprintf("点击元素失败：%s", node.Xpath))
		return
	}

	// 如果此次点击不是翻页的场景则打印相关日志，翻页日志已经在handleLoopPaging中打印
	if !node.IsPaging {
		te.sendLog(constant.CLICK_ELEMENT, node.Xpath)
	}
}

func (te *TaskExecutor) handleCondition(node *bo.ConditionNode) {
	te.sendLog(constant.CONDITION, "进行条件判断")
	element := node.Ele
	// 是一个独立的Condition节点，意味着条件匹配的范围是整个Body
	if element == nil {
		body, err := te.wd.FindElement(selenium.ByCSSSelector, "body")
		if err != nil {
			te.sendLog(constant.EXCEPTION, "元素定位失败：body")
			return
		}
		element = body
	}

	for _, branch := range node.SubNodes {
		tmp := branch.(*bo.BranchNode)
		tmp.Ele = element
		te.beforeHandleNode(branch.(bo.NodeInf))
		// 成功匹配到一个分支
		match := te.handleBranch(tmp)
		te.judgeTaskExecStatus()
		if match || !te.execStatus {
			return
		}
	}
}

func (te *TaskExecutor) handleBranch(node *bo.BranchNode) bool {
	reg, err := regexp.Compile(node.Pattern)
	if err != nil {
		te.sendLog(constant.EXCEPTION, fmt.Sprintf("正则表达式编译失败：%s", node.Pattern))
		return false
	}

	text, err := node.Ele.Text()
	if err != nil {
		te.sendLog(constant.EXCEPTION, "获取元素文本失败")
		return false
	}

	if reg.MatchString(text) {
		te.sendLog(constant.BRANCH, fmt.Sprintf("成功匹配分支，文本匹配：%s", node.Pattern))
		for _, subNode := range node.SubNodes {
			if clickElementNode, ok := subNode.(*bo.ClickElementNode); ok && clickElementNode.Xpath == "" {
				clickElementNode.Ele = node.Ele
			}
			if extractDataNode, ok := subNode.(*bo.ExtractDataNode); ok && extractDataNode.Xpath == "" {
				extractDataNode.Ele = node.Ele
			}
			te.exec(subNode)
			if !te.execStatus {
				return false
			}
		}
		return true
	}
	te.sendLog(constant.BRANCH, "未成功匹配分支")
	return false
}

func (te *TaskExecutor) sendLog(logCategory string, log string) {
	var color string
	switch logCategory {
	case constant.EXTRACT_DATA:
		color = constant.GREEN
	case constant.WAIT, constant.TASK_CONTINUE:
		color = constant.YELLOW
	case constant.EXCEPTION, constant.TASK_PAUSE:
		color = constant.RED
	default:
		color = constant.WHITE
	}
	te.msgSendStream <- tools.LogFormat(color, logCategory, log)
}

// waitDynamic 等待动态内容加载
// 在与页面元素进行交互（例如点击）或进行页面导航时往往会触发大量动态内容加载，需要等待页面加载完成后才能继续后续操作，否则一些操作（例如提取数据）会因为定位不到元素而失败
// js向后端发送请求的方式多种多样，例如原生XmlHttp，ajax,axios,graphql,fetch等，其中ajax(jquery)提供了jQuery.active这个全局状态用于监控活跃的ajax请求。
// 对于其他的方式我们无法得知活跃的请求数，即便通过注入js代码进行统计也无法覆盖到加载页面时的请求
// 这里默认只对ajax类型请求进行等待，其余的方式需要用户来处理（添加等待时间，等待指定元素等）
func (te *TaskExecutor) waitDynamic() {
	result, err := te.wd.ExecuteScript(`
	if (window.jQuery || window.$) {
		return jQuery.active === 0;
	} 
	return true
	`, nil)
	if err != nil {
		return
	}
	if result.(bool) {
		return
	}
	te.sendLog(constant.WAIT, "等待Ajax请求")
	_ = te.wd.WaitWithTimeoutAndInterval(func(wd selenium.WebDriver) (bool, error) {
		result, err := wd.ExecuteScript(`
	if (window.jQuery || window.$) {
		return jQuery.active === 0;
	} 
	return true
	`, nil)
		if err != nil {
			return true, err
		}
		if result.(bool) {
			return true, nil
		}
		return false, nil
	}, 10*time.Second, 200*time.Millisecond)
	te.sendLog(constant.WAIT, "Ajax请求完成")
}

// beforeHandleNode 节点操作预处理
func (te *TaskExecutor) beforeHandleNode(nodeGetter bo.NodeInf) {
	node := nodeGetter.GetNode()
	// 默认检测并等待Ajax动态内容
	te.waitDynamic()
	// 进行用户自定义的等待
	if node.BeforeExecIsRandom {
		timeWait := rand.Intn(5) + 1
		te.sendLog(constant.WAIT, fmt.Sprintf("等待%d秒", timeWait))
		time.Sleep(time.Duration(timeWait) * time.Second)
		te.sendLog(constant.WAIT, "完成等待")
	} else if node.BeforeExec > 0 {
		te.sendLog(constant.WAIT, fmt.Sprintf("等待%d毫秒", node.BeforeExec))
		time.Sleep(time.Millisecond * time.Duration(node.BeforeExec))
		te.sendLog(constant.WAIT, "完成等待")
	}
	// TODO 等待指定元素出现（如果有的话）

}

func NewTaskExecutor(taskExec *bo.TaskExec,
	msgSendStream chan string, dataStream chan map[string]any, done chan struct{}, controlStream chan bool) (*TaskExecutor, error) {
	// 设置浏览器选项
	caps := selenium.Capabilities{
		"browserName": "chrome",
		"goog:chromeOptions": chrome.Capabilities{
			Args: []string{
				// 开启无头模式, 不显示浏览器
				"--headless",
			},
		},
	}
	// 创建webdriver实例
	wd, err := selenium.NewRemote(caps, selenium.DefaultURLPrefix)
	if err != nil {
		msgSendStream <- tools.LogFormat(constant.RED, constant.EXCEPTION, "WebDriver实例创建失败")
		return nil, err
	}

	return &TaskExecutor{
		nodes:         taskExec.Nodes,
		wd:            wd,
		done:          done,
		controlStream: controlStream,
		msgSendStream: msgSendStream,
		dataStream:    dataStream,
		execStatus:    true,
	}, nil
}
